/*
 * Copyright (c) 2012 Sean Connelly
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
 * associated documentation files (the "Software"), to deal in the Software without restriction,
 * including without limitation the rights to use, copy, modify, merge, publish, distribute,
 * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies or
 * substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
 * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

var ubig = require('./ubig.js');
var big = require('./big.js');

//
// number only limited by available memory, stored as a fraction with a sign
//

function ratio_create(num_neg, ubig_top, ubig_bot)
{
	function simplify(obj)
	{
		if (ubig_bot.isZero())
		{
			if (!ubig_top.isZero())
				ubig_top.setInt(1);
			return obj;
		}
		var gcd = ubig_top.gcd(ubig_bot);
		ubig_top.div(gcd);
		ubig_bot.div(gcd);
		return obj;
	}

	var obj = {
			_getTop: function() {
					return ubig_top;
				},
			_getBot: function() {
					return ubig_bot;
				},
			copy: function(r) {
					num_neg = r.isNeg();
					ubig_top.copy(r._getTop());
					ubig_bot.copy(r._getBot());
					return this;
				},
			isNeg: function() {
					return !ubig_top.isZero() && num_neg;
				},
			isWhole: function() {
					return ubig_bot.isOne();
				},
			isPos: function() {
					return !ubig_top.isZero() && !num_neg;
				},
			isZero: function() {
					return ubig_top.isZero() && ubig_bot.isOne();
				},
			isOne: function() {
					return !num_neg && ubig_top.isOne() && ubig_bot.isOne();
				},
			isNegOne: function() {
					return num_neg && ubig_top.isOne() && ubig_bot.isOne();
				},
			isEven: function() {
					return this.isWhole() && ubig_top.isEven();
				},
			isOdd: function() {
					return this.isWhole() && ubig_top.isOdd();
				},
			isNaN: function() {
					return ubig_top.isZero() && ubig_bot.isZero();
				},
			isInf: function() {
					return !num_neg && ubig_top.isOne() && ubig_bot.isZero();
				},
			isNegInf: function() {
					return num_neg && ubig_top.isOne() && ubig_bot.isZero();
				},
			setInt: function(num) {
					num_neg = num < 0;
					ubig_top.setInt(Math.abs(num));
					ubig_bot.setInt(1);
					return this;
				},
			setNaN: function() {
					num_neg = false;
					ubig_top.setInt(0);
					ubig_bot.setInt(0);
					return this;
				},
			setInf: function() {
					num_neg = false;
					ubig_top.setInt(1);
					ubig_bot.setInt(0);
					return this;
				},
			setNegInf: function() {
					num_neg = true;
					ubig_top.setInt(1);
					ubig_bot.setInt(0);
					return this;
				},
			num: function() { // MYTODO: smarter?
					return (num_neg ? -1 : 1) * ubig_top.num() / ubig_bot.num();
				},
			neg: function() {
					num_neg = !num_neg;
					return this;
				},
			_addsub: function(r, flipR) {
					// case 1: -this + -r => -(this + r) (sign is maintained)
					// case 2: this + r (sign is maintained)
					// case 3: -this + r => r - this (sign toggles if r > this)
					// case 4: this + -r => this - r (sign toggles if r > this)
					var r_t = ubig.createCopy(r._getTop());
					r_t.mul(ubig_bot);
					ubig_top.mul(r._getBot());
					ubig_bot.mul(r._getBot());
					if ((!flipR && num_neg == r.isNeg()) ||
						(flipR && num_neg != r.isNeg())) // case 1 or 2
					{
						ubig_top.add(r_t);
					}
					else // case 3 or 4
					{
						var cmp = ubig_top.cmp(r_t);
						if (cmp == 0)
							return this.setInt(0);
						else if (cmp > 0) // this > r, sign stays the same
							ubig_top.sub(r_t);
						else // r > this, sign is toggled
						{
							num_neg = !num_neg;
							ubig_top = r_t.sub(ubig_top);
						}
					}
					return simplify(this);
				},
			add: function(r) {
					if (r.isZero())
						return this;
					if (this.isZero())
						return this.copy(r);
					return this._addsub(r, false);
				},
			sub: function(r) {
					if (r.isZero())
						return this;
					if (this.isZero())
						return this.copy(r).neg();
					return this._addsub(r, true);
				},
			mul: function(r) {
					if (r.isNeg())
						num_neg = !num_neg;
					ubig_top.mul(r._getTop());
					ubig_bot.mul(r._getBot());
					return simplify(this);
				},
			div: function(r) {
					if (r.isNeg())
						num_neg = !num_neg;
					ubig_top.mul(r._getBot());
					ubig_bot.mul(r._getTop());
					return simplify(this);
				},
			mod: function(r) {
					var r_t = ubig.createCopy(r._getTop());
					r_t.mul(ubig_bot);
					ubig_top.mul(r._getBot());
					ubig_bot.mul(r._getBot());
					// convert the tops to ubig and perform mod
					var res = big.createFromUbig(num_neg, ubig_top)
						.mod(big.createFromUbig(r.isNeg(), r_t));
					num_neg = res.isNeg();
					ubig_top = res._getUbig();
					return simplify(this);
				},
			pow: function(r) {
					if (r.isZero())
						return this.setInt(1);
					if (r.isNeg())
					{
						var t = ubig_top;
						ubig_top = ubig_bot;
						ubig_bot = t;
					}
					if (r.isOne() || r.isNegOne())
						return this;
					if (num_neg && r._getBot().isOne() && r._getTop().isEven())
						num_neg = !num_neg;
					ubig_top.pow(r._getTop());
					ubig_bot.pow(r._getTop());
					if (r._getBot().isOne())
						return simplify(this);
					// MYTODO: specification should state something to the effect of
					// converting the numbers to double to perform the n-th root, then
					// converting them back to a ratio... though not exactly how it is
					// implemented below because this code will result in unnecessary
					// loss of percision
					return this.copy(ratio_createFloat(Math.pow(this.num(), 1 / r._getBot().num())));
				},
			sgn: function() {
					return this.isZero() ? 0 : (num_neg ? -1 : 1);
				},
			cmp: function(r) {
					var r2 = ratio_createCopy(this).sub(r);
					if (r2.isZero())
						return 0;
					return r2._n ? -1 : 1;
				},
			toString: function() { // MYTODO: smarter?
					return '' + this.num();
				},
			dump: function() {
					return (this.isNeg() ? '-' : '') + ubig_top.toString() + '/' + ubig_bot.toString();
				}
		};

	return simplify(obj);
}

function ratio_createCopy(num)
{
	return ratio_create(num.isNeg(), ubig.createCopy(num._getTop()), ubig.createCopy(num._getBot()));
}

function ratio_createInt(num)
{
	return ratio_create(num < 0, ubig.createInt(Math.abs(num)), ubig.createInt(1));
}

function ratio_createIntRatio(top, bot)
{
	return ratio_create((top < 0 && bot >= 0) || (top >= 0 && bot < 0),
		ubig.createInt(Math.abs(top)), ubig.createInt(Math.abs(bot)));
}

function ratio_createFloat(num) // MYTODO: smarter?
{
	return ratio_createIntRatio(Math.floor(num * 100000), 100000);
}

function ratio_createBase(str, base)
{
	str = str.split('.');
	if (str.length == 1)
		str.push('');
	if (str.length != 2)
		throw 'invalid string';
	var top = big.createBase(str[0], base);
	var bot;
	if (str[1].length > 0)
	{
		bot = ubig.createInt(base).pow(ubig.createInt(str[1].length));
		top._getUbig().mul(bot).add(ubig.createBase(str[1], base));
	}
	else
		bot = ubig.createInt(1);
	return ratio_create(top.isNeg(), top._getUbig(), bot);
}

function ratio_createDec(decStr)
{
	return ratio_createBase(decStr, 10);
}

function ratio_createNaN()
{
	return ratio_createIntRatio(0, 0);
}

function ratio_createInf()
{
	return ratio_createIntRatio(1, 0);
}

function ratio_createNegInf()
{
	return ratio_createIntRatio(-1, 0);
}

exports.createCopy = ratio_createCopy;
exports.createInt = ratio_createInt;
exports.createIntRatio = ratio_createIntRatio;
exports.createFloat = ratio_createFloat;
exports.createBase = ratio_createBase;
exports.createDec = ratio_createDec;
exports.createNaN = ratio_createNaN;
exports.createInf = ratio_createInf;
exports.createNegInf = ratio_createNegInf;
